iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Software Development

打造 OS Kernel:從 OS in 1,000 Lines 到 xv6系列 第 18 篇

Day 18|從 VirtIO 到檔案:把 Tiny Kernel 變成一個小型 OS

  • 分享至 

  • xImage
  •  

今天的完成畫面只有一行:Hello from hello.txt on VirtIO disk!。
不過,這行文字不能來自使用者程式裡的字串,也不能由核心直接印出預先寫好的答案。
它必須存在磁碟映像中,經過裝置驅動與檔案查找,再交到 U-mode 程式手上。

我們會把這條路徑拆成三個檢查點:先確認磁碟內容,再確認核心讀取,最後確認資料能安全返回使用者空間。

一個檔名背後有兩種抽象

《Operating System Concepts》第 10 版第 12.2 節介紹核心如何與 I/O 裝置交換控制與資料,第 13.1 節則從檔案的角度描述具名資料集合。
兩者不是同一層:區塊裝置理解的是 sector 編號,應用程式關心的卻是檔名與內容。

今天 disk.c 的 disk_read() 負責讀 sector,fs_read() 負責把檔名轉成位置與長度。
把兩層分開後,我們才看得出「磁碟能讀」與「檔案找得到」分別在哪裡成立。

VirtIO 的入門路線參考《OS in 1,000 Lines》〈磁碟 I/O〉,檔案層則採本系列自訂的兩個 sector 格式。
它不是該教材檔案系統章節使用的 TAR 格式,也不是 FAT 或 ext4,不能交給一般掛載工具當成標準檔案系統。

Checkpoint 1:先做出可檢查的磁碟

在 30-days-os-kernel/examples/tiny-kernel/ 執行:

make DAY=18
ls -lh build/day18/disk.img
od -Ax -tx1 -N 32 build/day18/disk.img
dd if=build/day18/disk.img bs=512 skip=1 count=1 status=none | strings

最後一行只讀取專案內的映像檔,不涉及實體磁碟。
預期檔案大小是 1,024 bytes,第二個 sector 能找到 Hello from hello.txt on VirtIO disk!。

mkdisk.py 產生的格式如下:

位置 大小 內容
Sector 0,offset 0 8 bytes Magic:TINYFS1 與結尾 NUL
Sector 0,offset 8 16 bytes NUL 結尾的 hello.txt 檔名欄位
Sector 0,offset 24 4 bytes 檔案長度,little-endian
Sector 0,offset 28 4 bytes 資料 sector 編號,目前固定為 1
Sector 1 512 bytes 檔案內容與補齊空間

這個格式只容納一個唯讀檔案,沒有目錄、空間配置或寫入復原機制。
簡化後的好處是可以直接檢查每個欄位,完整看見檔案抽象如何落到區塊位置。

Checkpoint 2:讓 QEMU 提供 VirtIO Block Device

Day 18 的 Makefile 會在原本 RV32 啟動參數之外加入:

-global virtio-mmio.force-legacy=true \
-drive file=build/day18/disk.img,if=none,format=raw,id=labdisk \
-device virtio-blk-device,drive=labdisk,bus=virtio-mmio-bus.0

這是參數片段,實際執行請使用後面的 make DAY=18 run。
我們明確選擇 legacy VirtIO MMIO 介面,驅動也會確認 MMIO version 等於 1。
後面 xv6 的驅動使用 modern 介面,不應把兩邊的初始化暫存器混用。

disk_init() 會搜尋 QEMU virt 的 VirtIO MMIO slots,檢查 magic 與 block device ID,再設定 queue。
頁表已經映射這些 MMIO 範圍,否則核心可能在讀取裝置資訊前就先發生 page fault。

裝置拿到的是描述子,不是 C 函式呼叫

一次讀取請求使用三個串接的 descriptor:

請求 header(讀取操作、sector 編號)
        → 512-byte 接收緩衝區
        → 1-byte 完成狀態

第一項由裝置讀取,後兩項允許裝置寫入。
驅動填好 descriptor 與 available ring,通知裝置,再輪詢 used ring 是否有完成項目。

本次 queue size 為 8,但同一時間只送出一個同步請求。
共享欄位存取搭配記憶體屏障,避免只把 C 程式的敘述順序當成裝置必然看到的順序。
傳給裝置的是實體位址,目前依靠核心的 identity mapping 取得,還沒有一般化的 DMA mapping 或 IOMMU 管理。

輪詢先求看得見,不代表最有效率

驅動在等待迴圈中檢查完成狀態,超過迴圈上限就 panic,避免無限等待。
這個上限不是精確的毫秒 timeout,因為迴圈速度會受模擬環境影響。
我們尚未把 I/O 等待接到中斷或 Day 13 的 WAITING 狀態,CPU 在這段時間仍忙於輪詢。

這正是恐龍書中 polling 與 interrupt-driven I/O 差異的可觀察版本:先把資料讀對,再逐步改善等待方式。

Checkpoint 3:把內容交給使用者程式

Day 17 的 syscall dispatcher 新增編號 3,介面是:

欄位 約定
a0 使用者接收緩衝區的 VA
a1 緩衝區容量
回傳 a0 成功時的 bytes 數,失敗為 −1

這個呼叫固定讀取 hello.txt,一次最多 128 bytes。
它還不是具有 file descriptor 與檔案偏移量的一般 read()。
User 程式在堆疊上保留緩衝區,取得內容後,再透過編號 1 逐字輸出。

核心不能直接相信 a0 是合法指標。
user.c 的 copyout() 先檢查範圍加法是否溢位,再確認整段目的範圍都映射到 U/W 頁面,全部通過後才開始複製。

實際寫入使用轉譯後 PA 的核心 identity alias,並沒有開啟 SUM 直接存取 user VA。
這份逐 byte 檢查刻意容易閱讀,但不適合大量資料搬移,也未涵蓋多核心同時修改頁表的情況。

執行完整路徑

仍在 30-days-os-kernel/examples/tiny-kernel/ 執行:

make DAY=18 run

預期在頁表訊息之後看到:

missing file and kernel-pointer rejection=ok
enter U-mode
Hello from hello.txt on VirtIO disk!
user exit=0

第一行來自兩個核心端檢查:不存在的檔案應回傳失敗,將核心位址當成使用者接收緩衝區也必須被拒絕。
它們是 helper 層的負向測試,不是已經用惡意 U-mode 程式測過所有輸入。
後面的文字則確實經過磁碟讀取與 syscall 返回路徑。

想確認輸出真的來自磁碟,可以修改 mkdisk.py 的訊息,重新執行 make DAY=18 run。
Makefile 會依腳本更新磁碟映像,使用者程式的輸出迴圈不必跟著修改。
訊息請維持在本次 128-byte 讀取容量內。

如果停在 queue 等待,先檢查 force-legacy=true、descriptor 權限與 MMIO slot。
如果是找不到檔案,先回頭檢查 header,而不是立即修改 Trap 或頁表程式。
測試後按 Ctrl+A 再按 X 離開 QEMU。

第一個階段到這裡完成了什麼?

我們已有能各自重現的排程、頁表與使用者模式實驗,今天又接通了從 user request 到磁碟內容的路徑。
不過,多使用者行程、搶占式排程、一般檔案 API,以及可寫檔案系統仍未整合,這還不是可替代 xv6 的完整系統。

本日新增或擴充的檔案是 disk.c、mkdisk.py、user.c、user.S 與 Makefile 的磁碟參數。
建議 commit:

day18: read a tiny file through virtio and a user syscall

Day 19 會帶著這些具體經驗進入 xv6,看看較完整的核心如何把相同責任組合在一起。

參考資料


上一篇
Day 17|System Call:User Program 第一次向 Kernel 求助
下一篇
Day 19|進入 xv6:把自己寫的 Tiny OS 對映到 Unix-like Kernel
系列文
打造 OS Kernel:從 OS in 1,000 Lines 到 xv6 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言